moddbfandomcom-20200213-history
VBNET:Class XMLINI
In VB6, one way to store data has long been the elderly INI file. Since Windows 95, Microsoft has pushed hard to get rid of the INI file, and move that data into the registry. Many developers are wary of that approach, fearful of contributing to the "clutter" of the registry. So the INI file has survived despite Microsoft's best efforts. And there are other advantages to an INI file. In Rogue (a game we're working on over at Atomic Monks), its XML file holds video settings, key mappings, and more. It's much quicker to open Notepad and change an INI file than it is to modify registry entries. But INI support in .NET falls outside of managed code...so if you want a "real" .NET way of doing this, you need to go to XML. Let's look at the video section of Rogue's INI file: Video Height=768 Width=1024 Windowed=1 TripleBuffer=0 And the XML file for the same entries: The structure is easy enough to follow, and is modeled after the INI file structure (different sections, with numerous key/value sets). The hard part of dealing with the XML itself will be handled by the xmlDocument object. Let's first take a look at the class declarations and the New procedure(s): Public Class CXMLINI Private doc As New Xml.XmlDocument Private xmlfile As String Public Sub New(ByVal Filename As String) ' open xml file for use Try doc.Load(Filename) xmlfile = Filename Catch ex As Exception End Try End Sub Public Sub New() ' open default xml file for use Dim s As String s = System.AppDomain.CurrentDomain.BaseDirectory s &= Application.ProductName s &= ".xml" Try doc.Load(s) xmlfile = s Catch ex As Exception End Try End Sub doc is the xmlDocument object for the class, which we'll maintain until the class is destroyed. This object stores the contents of the xml file we load in memory, where we can almost treat it like a dataset. Notice that I've overloaded the New procedure. When given a filename, it opens that xml file. Otherwise, it defaults to Application Name.xml. In either case, if the file doesn't exist, it catches the error and goes on, but remembers the file name that it was supposed to use (for later use). In the class destructor, Finalize, we see why we saved that name: Protected Overrides Sub Finalize() ' save changes to settings here, if we haven't already Try ' save changes to data doc.Save(xmlfile) Catch ex As Exception End Try MyBase.Finalize() End Sub Any changes made will be saved automatically here, so if the file didn't exist before, it will now. Now for GetSetting, which should be a familiar command for this sort of thing: Public Function GetSetting(ByVal Section As String, ByVal Key As String, _ ByVal DefaultValue As String) As String Dim node As Xml.XmlNode Try node = doc.SelectSingleNode("/sections/section& _ Section & "'/item& Key & "'") If node Is Nothing Then GetSetting = DefaultValue Else GetSetting = node.Attributes("value").Value End If Catch ex As Exception End Try End Function Here we see our first instance of the xmlNode object, which represents a single line of our XML file. With the SelectSingleNode method we can attach to a particular item in a particular section. The format for the search is: /sections/section@name='sectionname'/item@key='keyname' The brackets are how you specify an element parameter to match ("name" for section and "key" for items, as seen in the sample XML file at the beginning of this article). It will return nothing if there is no match...otherwise it returns a node, from which we can read the "value" attribute to return from the function. The full version of the class I created includes several overloads for GetSetting to more cleanly handle different data types, such as Boolean and Integer. They convert the passed parameters to strings, call the "root" GetSetting function, and convert the returned string as Boolean or Integer, as appropriate. The other important piece of this is SaveSetting, which is only slightly more involved than GetSetting: Public Sub SaveSetting(ByVal Section As String, ByVal Key As String, _ ByVal NewValue As String) Dim node As Xml.XmlNode, keynode As Xml.XmlNode Try ' check for/create section node = doc.SelectSingleNode("/sections/section& Section & "'") If node Is Nothing Then ' section doesn't exist, create 'Original author forgot to create the beginning structure :P Dim dec As XmlDeclaration = doc.CreateXmlDeclaration("1.0", Nothing, Nothing) doc.AppendChild(dec) Dim docroot As XmlElement = doc.CreateElement("sections") doc.AppendChild(docroot) node = doc.SelectSingleNode("/sections") Dim newnode As Xml.XmlNode = doc.CreateElement("section") Dim att As Xml.XmlAttribute = doc.CreateAttribute("name") att.Value = Section newnode.Attributes.Append(att) node.AppendChild(newnode) node = doc.SelectSingleNode("/sections/section& Section & "'") End If ' get key keynode = doc.SelectSingleNode("/sections/section& _ Section & "'/item& Key & "'") If keynode Is Nothing Then ' create key Dim newnode As Xml.XmlNode = doc.CreateElement("item") Dim att As Xml.XmlAttribute = doc.CreateAttribute("key") att.Value = Key newnode.Attributes.Append(att) att = doc.CreateAttribute("value") att.Value = NewValue newnode.Attributes.Append(att) node.AppendChild(newnode) Else ' just update key value keynode.Attributes("value").Value = NewValue End If Catch ex As Exception End Try End Sub With a first call of SelectSingleNode, SaveSetting checks to see if the required section exists. If not, then it creates it with the CreateElement method of xmlDocument. It then sets the name of the section by creating a new xmlAttribute object, which it appends to the new section node. The same thing is done for the actual key next, with one more attribute (since the items have "key" and "value"). In either case, if the section or item already exists, it simply updates the attributes accordingly. Again, my class has some overloads for SaveSetting to handle Boolean and Integer values. Improvements might include doing a SaveSetting when a GetSetting is going to return a default value. This would create and populate the wanted XML file, data and all, if none existed.